package ru.stepanovdn.trading.server.service.candle

import jakarta.annotation.PostConstruct
import jakarta.persistence.EntityManager
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.DependsOn
import org.springframework.stereotype.Service
import ru.stepanovdn.trading.server.entity.Candle
import ru.stepanovdn.trading.server.repository.CandleRepository
import ru.stepanovdn.trading.server.service.tinkoff.ITinkoffCommonAPI
import ru.stepanovdn.trading.server.strategy.AInstrumentByFiatStrategy
import ru.stepanovdn.trading.server.strategy.AStrategy
import ru.stepanovdn.trading.server.strategy.StrategySelector
import ru.tinkoff.piapi.contract.v1.CandleInterval
import ru.tinkoff.piapi.contract.v1.HistoricCandle
import java.math.BigDecimal
import java.time.Duration
import java.time.OffsetDateTime
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
import java.util.function.Function
import java.util.stream.Collectors
import java.util.stream.LongStream

/**
 * Service to load and store actual history of candles
 */
@Service
@DependsOn("instrumentService")
class CandleHistoryService (
    @Autowired val tinkoffCommonAPI: ITinkoffCommonAPI,
    @Autowired val strategySelector: StrategySelector,
    @Autowired val candleRepository: CandleRepository,
    @Autowired val entityManager: EntityManager
) {
    private val log = LoggerFactory.getLogger(javaClass)

    private var firstCandleDateTimeByFigi: ConcurrentHashMap<String, OffsetDateTime>? = null

    private var candlesLocalCache: Map<String, List<Candle>>? = null

    @Value("\${candle.history.duration}")
    private val historyDuration: Duration? = null

    @Value("\${candle.listener.enabled:false}")
    private val isCandleListenerEnabled: Boolean? = null

    @Transactional(Transactional.TxType.REQUIRES_NEW)
    fun addOrReplaceCandles(newCandle: HistoricCandle, figi: String?): Candle {
        try {
            val dateTime: OffsetDateTime = ConvertorUtils.toOffsetDateTime(newCandle.time.seconds)
            val closingProse: BigDecimal = ConvertorUtils.toBigDecimal(newCandle.close, null)
            val openPrice: BigDecimal = ConvertorUtils.toBigDecimal(newCandle.open, null)
            val lowestPrice: BigDecimal = ConvertorUtils.toBigDecimal(newCandle.low, null)
            val highestPrice: BigDecimal = ConvertorUtils.toBigDecimal(newCandle.high, null)

            log.trace("Candle for {} by {} with openPrice {}, closingProse {}", figi, dateTime, openPrice, closingProse)
            var candle: Candle? = candleRepository!!.findByFigiAndIntervalAndDateTime(figi, "1min", dateTime)
            if (candle != null) {
                if (closingProse.compareTo(candle.closingPrice) != 0 || highestPrice.compareTo(
                        candle.highestPrice
                    ) != 0 || openPrice.compareTo(
                        candle.openPrice
                    ) != 0 || lowestPrice.compareTo(candle.lowestPrice) != 0
                ) {
                    log.trace("Replaced existing candle {} to candle {}:", candle, newCandle)
                    candle.closingPrice = closingProse
                    candle.highestPrice = highestPrice
                    candle.lowestPrice = lowestPrice
                    candle.openPrice = openPrice
                    candle = candleRepository.save(candle)
                }
                return candle
            }
            candle = Candle()
            candle.figi = figi
            candle.closingPrice = closingProse
            candle.highestPrice = highestPrice
            candle.lowestPrice = lowestPrice
            candle.openPrice = openPrice
            candle.interval = "1min"
            candle.dateTime = dateTime
            candle = candleRepository.save(candle)
            log.trace("Saved new candle {}", candle)
            return candle
        } catch (e: Exception) {
            log.error("Can't save new candle", e)
            throw e
        }
    }

    fun getCandlesByFigiBetweenDateTimes(
        figi: String,
        startDateTime: OffsetDateTime?,
        endDateTime: OffsetDateTime?
    ): List<Candle> {
        // если слушатель выключен - значит запускаем не в продакшн режиме, можем хранить свечки в памяти, а не на диске (БД)
        if (!isCandleListenerEnabled!!) {
            val candlesByFigi: List<Candle> = candlesLocalCache!![figi]!!
            if (candlesByFigi.isEmpty()) {
                throw UnsupportedOperationException("Candles not found in local cache for $figi")
            }
            return candlesByFigi.stream()
                .filter { c: Candle ->
                    c.dateTime!!.isAfter(startDateTime)
                }
                .filter { c: Candle ->
                    c.dateTime!!.isBefore(endDateTime) || c.dateTime!!.isEqual(endDateTime)
                }
                .collect(Collectors.toList())
        }
        return candleRepository!!.findByFigiAndIntervalAndBetweenDateTimes(figi, "1min", startDateTime, endDateTime)
    }

    private fun requestCandles(
        figi: String,
        start: OffsetDateTime,
        end: OffsetDateTime,
        resolution: CandleInterval,
        tries: Int
    ): List<HistoricCandle> {
        try {
            return tinkoffCommonAPI!!.api!!.marketDataService.getCandles(
                figi, start.toInstant(),
                end.toInstant(), resolution
            ).get()
        } catch (e: Exception) {
            log.error("Can't get candles for figi {}", figi, e)
            Thread.sleep(10000)
            return requestCandles(figi, start, end, resolution, tries - 1)
        }
    }

    fun requestCandlesHistoryForDays(days: Long) {
        if (days == 0L) {
            return
        }

        val now: OffsetDateTime = OffsetDateTime.now()
        val figies: Set<String> = strategySelector!!.figiesForActiveStrategies
        log.info("Start to load history {} days, for figies {}", days, figies)
        figies.forEach { figi ->
            LongStream.range(0, days).forEach { i: Long ->
                log.info("Request candles {}, {} day", figi, i)
                val candles: List<HistoricCandle> = requestCandles(
                    figi,
                    now.minusDays(i + 1),
                    now.minusDays(i),
                    CandleInterval.CANDLE_INTERVAL_1_MIN,
                    12
                )
                log.info("Save candles to DB {}, {} day", figi, i)
                candles.forEach(Consumer { c: HistoricCandle -> addOrReplaceCandles(c, figi) })
                log.info("Candles was saved to DB {}, {} day", figi, i)
            }
        }
        log.info("Loaded to load history for {} days", days)
    }

    fun getFirstCandleDateTime(figi: String): OffsetDateTime {
        return firstCandleDateTimeByFigi!!.computeIfAbsent(figi, Function<String, OffsetDateTime> { f: String ->
            val firstCandle: Candle = candleRepository!!.findByFigiAndIntervalOrderByDateTime(f, "1min").first()
            firstCandle.dateTime
        })
    }

    @PostConstruct
    fun init() {
        firstCandleDateTimeByFigi = ConcurrentHashMap<String, OffsetDateTime>()
        val historyDurationByStrategies: Duration = strategySelector.activeStrategies!!.stream()
            .filter { s -> s.type === AStrategy.Type.instrumentByFiat }
            .map { s -> (s as AInstrumentByFiatStrategy).historyDuration }
            .reduce { d1, d2 -> if (d1.toSeconds() > d2.toSeconds()) d1 else d2 }
            .stream().findFirst().orElse(Duration.ZERO)
        requestCandlesHistoryForDays(
            if (historyDurationByStrategies.seconds > historyDuration!!.toSeconds()
            ) historyDurationByStrategies.toDays() else historyDuration.toDays()
        )

        if (!isCandleListenerEnabled!!) {
            val candles: List<Candle> = candleRepository!!.findByIntervalOrderByDateTime("1min")
            candlesLocalCache = candles.stream()
                .peek { o: Candle? -> entityManager!!.detach(o) }
                .collect(Collectors.groupingBy(Candle::figi))
        }
    }
}
